Login / Logout Flow: Android Jetpack Compose and CompositionLocal

Login / Logout Flow: Android Jetpack Compose and CompositionLocal

CompositionLocal is useful when you want to create a dependency in a higher node of the layout tree and use it on a lower node without having to pass it down the tree through every child Composable.

Here we will use it to direct our application when a user logs in and out.

  1. When the user is successfully logged in (isLoggedIn = true), they are directed to the rest of the application views.
  2. When a user is logged out (isLoggedIn = false) at any point within the app, they are directed to the login page.

1_6VAswSMtIhBP6zBwzvPShg.png

We need the following components:

  1. User State View Model
  2. Application Switcher
  3. Login Screen
  4. Home Screen

User State View Model

The UserStateViewModel keeps track of and broadcasts our user status. We are going to store this view model to a CompositionLocal.

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.delay


class UserStateViewModel : ViewModel() {
    var isLoggedIn by mutableStateOf(false)
    var isBusy by mutableStateOf(false)

    suspend fun signIn(email: String, password: String) {
        isBusy = true
        delay(2000)
        isLoggedIn = true
        isBusy = false
    }

    suspend fun signOut() {
        isBusy = true
        delay(2000)
        isLoggedIn = false
        isBusy = false
    }
}

val UserState = compositionLocalOf<UserStateViewModel> { error("User State Context Not Found!") }

We make our view model instance available to all child composables, starting from the ApplicationSwitcher composable

class MainActivity : ComponentActivity() {
    private val userState by viewModels<UserStateViewModel>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CompositionLocalProvider(UserState provides userState) {
                ApplicationSwitcher()
            }
        }
    }
}

Application Switcher

@Composable
fun ApplicationSwitcher() {
    val vm = UserState.current
    if (vm.isLoggedIn) {
        HomeScreen()
    } else {
        LoginScreen()
    }
}

Login Screen

The Login screen can now use the UserStateViewModel to invoke signIn

@Composable
fun LoginScreen() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    val coroutineScope = rememberCoroutineScope()
    val vm = UserState.current
    Column(
        Modifier
            .fillMaxSize()
            .padding(32.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        if (vm.isBusy) {
            CircularProgressIndicator()
        } else {
            Text("Login Screen", fontSize = 32.sp)
            Spacer(modifier = Modifier.height(16.dp))
            Email(email, onChange = {
                email = it
            })
            Spacer(modifier = Modifier.height(16.dp))
            Password(password, onChange = {
                password = it
            })
            Spacer(modifier = Modifier.height(16.dp))
            LoginButton(onClick = {
                coroutineScope.launch {
                    vm.signIn(email, password)
                }
            })
        }


    }
}

Home Screen with Logout Button

The Home screen also uses the UserStateViewModel to invoke the signOut

@Composable
fun HomeScreen(navController: NavHostController) {
    val vm = UserState.current
    val coroutineScope = rememberCoroutineScope()
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Home") },
                actions = {
                    IconButton(onClick = {
                        coroutineScope.launch {
                            vm.signOut()
                        }
                    }) {
                        Icon(Icons.Filled.ExitToApp, null)
                    }
                }
            )
        },
        ) {
        Column(
            Modifier
                .fillMaxSize()
                .padding(16.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            if (vm.isBusy) {
                CircularProgressIndicator()
            } else {
                Text("Home")
            }
        }
    }
}

At all times the Application Switcher is monitoring the isLoggedIn status:

1_8eRY-tsLv8eDl2JkzSmybw.gif