How to use the combine() function to combine states in a sample Login App?

9Qv7...HJpG
9 Feb 2024
52



Helloooo guys ✌ Today I will explain the use of the combine() function with a sample Login application. In general, there will be a Login screen in the application and it will receive email and password from the user. According to these values, we will examine some states before pressing the Login button and we will separate them as UI states. Let’s start step by step.
Firstly, let me explain what is the purpose of the combine() function.


combine() = Returns a Flow whose values are generated with the transform function by combining the most recently emitted values by each flow.

Let’s look at what it receives as inputs and what it returns.

public fun <T1, T2, R> combine(
    flow: Flow<T1>,
    flow2: Flow<T2>,
    transform: suspend (T1, T2) -> R
): Flow<R>


After doing the above definition, I can say for what purpose I will use this function. As you know in apps, there are data that we can get from two different sources and manage with state, and there are also user events.

The first of these is local sources of state change. In our sample application, these refer to the user’s states on the UI side. Email, password authentication, and isRememberMeClicked control are what we call local sources of state change.

→ I will convert these inputs to state as follows.

data class UserInputState(
    val emailValidation: Boolean = true,
    val passwordValidation: Boolean = true,
    val isRememberMeChecked: Boolean = false,
)

→ Then I will use UserInputState with a MutableStateFlow() in the ViewModel.

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    var userInputState = MutableStateFlow(UserInputState())

}

→ On the screen side, I will make their assignments. After the user presses the Login button, I will call the viewModel instance and make changes to the states in it.

ButtonComponent(
    value = R.string.login_button_message,
    onClick = {
        if(userEmail.isNotEmpty() && userPassword.isNotEmpty() && userPassword.length >= 5){
            viewModel.userInputState.value = UserInputState(emailValidation = true,passwordValidation = true,isRememberMeChecked = isRememberMeChecked)
            viewModel.login(userEmail,userPassword)
        } else {
            viewModel.userInputState.value = UserInputState(emailValidation = false,passwordValidation = false)
        }
    }
)

The operations I did above were to manage the states locally and directly with the UI event.
Next, I will show the operations in the data layer where I get the isUserLoggedIn() boolean variable. When we show that the UserEntity object saved is in the UserEntity list from the database, this user will be logged in and I checked this business logic in the ViewModel.

fun login(email: String, password: String) {
    viewModelScope.launch {
        userRepository.getUsers().collectLatest { userList ->
            val isUserLogin = userList.map { user ->
                UserEntity(email = user.email, password = user.password)
            }.contains(
                UserEntity(email = email, password = password)
            )
        }
    }
}

Let me briefly tell you what the above process does. I get all the users registered in the database from userRepository() and since this returns a flow, I get it by saying collectLatest. Then I use the email and password that will be useful to me by mapping this list and check it with the email and password I get with the function by saying contains(). If it exists, it will be assigned to a variable as true or false.
In the previous stage, we were able to manage the Local Source of State Change part with a user input state data class.

→ After these operations, I can proceed to the External Source of State Change section.

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

  var userInputState = MutableStateFlow(UserInputState())
  private var isUserLoggedInState = MutableStateFlow(userRepository.isUserLoggedIn())

  fun login(email: String, password: String) {
        viewModelScope.launch {
            userRepository.getUsers().collectLatest { userList ->
                val isUserLogin = userList.map { user ->
                    UserEntity(email = user.email, password = user.password)
                }.contains(
                    UserEntity(email = email, password = password)
                )
                isUserLoggedInState.value = userRepository.isUserLoggedIn(isUserLogin)
            }
        }
    }
}


After these procedures, I can move on to the External Source of State Change section. In the previous stage, we were able to manage the Local Source of State Change section with a user-input state data class. I assign the boolean value from the userRepository() I wrote to isUserLoggedInState with MutableStateFlow. In the Login function, I assign the isUserLogin value I received to the isUserLoggedInState.value.

There are states we received from two different sources. Now we move on to where we actually need to use the combine() function. I write a UI state as follows to both log in the user and control all validations on the UI side and combine them under a single state.

sealed interface LoginUiState {
    object Loading : LoginUiState
    data class UserLogin(
        val isUserLoggedIn: Boolean, // this input comes from data layer
        val emailValidation: Boolean, // this input comes from UI layer
        val passwordValidation: Boolean, // this input comes from UI layer
        val isRememberMeClicked: Boolean // this input comes from UI layer
    ) : LoginUiState
    object Failed : LoginUiState
}


The LoginUiState above will manage all states that may occur on the UI side for me. I created this to combine states from two different sources and manage them through a single UI state.
Let’s look at how it is defined in ViewModel.

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    var userInputState = MutableStateFlow(UserInputState())
    private var isUserLoggedInState = MutableStateFlow(userRepository.isUserLoggedIn())

    val uiState: StateFlow<LoginUiState> =
        combine(userInputState, _isUserLoggedInState) { userInputValidations, login ->
            when {
                !userInputValidations.emailValidation && !userInputValidations.passwordValidation -> {
                    LoginUiState.Failed
                }

                userInputValidations.emailValidation &&
                        userInputValidations.passwordValidation &&
                        userInputValidations.isRememberMeChecked &&
                        login -> {
                    LoginUiState.UserLogin(
                        isUserLoggedIn = login,
                        emailValidation = userInputValidations.emailValidation,
                        passwordValidation = userInputValidations.passwordValidation,
                        isRememberMeClicked = userInputValidations.isRememberMeChecked
                    )
                }

                else -> {
                    LoginUiState.Loading
                }
            }
        }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = LoginUiState.UserLogin(
                isUserLoggedIn = false,
                emailValidation = false,
                passwordValidation = false,
                isRememberMeClicked = false
            )
        )

    fun login(email: String, password: String) {
        viewModelScope.launch {
            userRepository.getUsers().collectLatest { userList ->
                val isUserLogin = userList.map { user ->
                    UserEntity(email = user.email, password = user.password)
                }.contains(
                    UserEntity(email = email, password = password)
                )
                isUserLoggedInState.value = userRepository.isUserLoggedIn(isUserLogin)
            }
        }
    }
}


Let me explain what the combine() function above does. It combines states from two different sources and returns the value of each. Here, according to the results, we can expose the LoginUIState we want with the when condition. Combine is gonna return a flow. This UI state should be collected in a lifecycle-aware manner in the UI.

On the screen side, let me show you how to collect this in a lifecycle-aware manner.

@Composable
fun LoginScreen(
    modifier: Modifier = Modifier,
    viewModel: LoginViewModel = hiltViewModel(),
    onRegisterTextClicked: () -> Unit = {}
) {
    val snackbarHostState = remember {SnackbarHostState()}
    val loginUiState by viewModel.uiState.collectAsStateWithLifecycle()
    var userEmail by rememberSaveable { mutableStateOf("") }
    var userPassword by rememberSaveable { mutableStateOf("") }
    var isRememberMeChecked by rememberSaveable { mutableStateOf(false) }

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        },
        content = { contentPadding ->

            Surface(
                color = Color.White,
                modifier = modifier
                    .fillMaxSize()
                    .background(Color.White)
                    .padding(28.dp)
                    .padding(contentPadding)
            ){
                when(loginUiState) {
                    is LoginUiState.Loading -> {
                        LoadingProgressBar()
                    }

                    is LoginUiState.Failed -> {
                        LoginFailedMessage()
                    }

                    is LoginUiState.UserLogin -> {
                        LoginSuccessMessage()
                    }
                }
              Column(...)
}


In Compose, we use the collectAsStateWithLifecycle() API that is gonna transform the collected values into Compose state, and then we can pass the stateless composable functions that are actually gonna render the information onto the screen.
I hope you like it 😀. Please let me know if there are any parts you see that are wrong or that you think I should add.

Have a good day all of you guys ✌.

Sources

State holders and UI State | Android Developers
The UI layer guide discusses unidirectional data flow (UDF) as a means of producing and managing the UI State for the…developer.android.com

Write & Read to Earn with BULB

Learn More

Enjoy this blog? Subscribe to yusufyildiz41

2 Comments

B
No comments yet.
Most relevant comments are displayed, so some may have been filtered out.