1

Looking at a huge number of lines-of-code to do a basic task:

First the imports:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions

import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable

import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation

import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField

import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.ui.tooling.preview.Preview

Then the TextFields:

@Composable
fun EmailTextField() {
    var email by rememberSaveable { mutableStateOf("") }

    TextField(
        value = email,
        onValueChange = { email = it },
        label = { Text("Enter email") }
    )
}

@Composable
fun PasswordTextField() {
    var password by rememberSaveable { mutableStateOf("") }

    TextField(
        value = password,
        onValueChange = { password = it },
        label = { Text("Enter password") },
        visualTransformation = PasswordVisualTransformation(),
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
    )
}

Finally the App itself:

@OptIn(ExperimentalResourceApi::class)
@Composable
@Preview
fun App() {
    MaterialTheme {
        var showContent by remember { mutableStateOf(false) }
        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            EmailTextField()
            PasswordTextField()
            Button(onClick = { showContent = !showContent }) {
                Text("Auth")
            }
        }
    }
}

Trying to get the email and password, pass it to a function that does an HTTP request and based on the output, navigate to a welcome screen or display the error.

14 files is what the official compose example project shows to solve this problem (and it doesn't even do the HTTP request/response).

Is there not just a simple way of getting the email+password and navigating to the next thing on form submission?

1 Answer 1

2

Is there not just a simple way of getting the email+password and navigating to the next thing on form submission?

The repository you linked is doing a little bit more than just a single login. It contains two different screens for signing in and signing up, and a welcome page that allows choosing between sign in / sign up.
Also, the repository aims to serve as a foundation for building more complex forms only with Compose libraries. If your form always is going to have just two TextField, then you can simplify the whole thing.

We can summarize first what files are doing what:

  • UI Definitions with their ViewModels:
    • WelcomeScreen.kt + WelcomeViewModel.kt
    • SignInScreen.kt + SignInViewModel.kt
    • SignUpScreen.kt + WelcomeViewModel.kt
    • SignInSignUpScreen.kt
  • Form Validation:
    • EmailState.kt
    • PasswordState.kt
    • TextFieldState.kt
  • Wrapper Composables for creating the ViewModels
    • WelcomeRoute.kt
    • SignInRoute.kt
    • SignUpRoute.kt
  • Repository
    • UserRepository.kt

Now let's analyze the potential to reduce complexity:

UI Definitions: no potential
It is a common design in Jetpack Compose that each main screen gets its own ViewModel.

Form Validation: big potential
If your TextFields are only going to have one error state (wrong password or email), then you can move that logic to the ViewModel classes.

Wrapper Composables: big potential
These Composables only have the purpose to generate a ViewModel with a UserRepository instance. This logic could be moved upwards into the NavHost, or removed entirely when using dependency injection like Hilt or Koin.

Repository: no potential
This is required.


So when you would typically implement this with simplicity as a goal, you would end up with ~8 files, which might be more acceptable. If you would only implement a single login page, you would end up with three files:

  • LoginScreen.kt
  • LoginViewModel.kt
  • UserRepository.kt

Regarding the topic of code verbosity that you mentioned:
Jetpack Compose unifies UI and layout definitions into single Composables. While the code at first might seem verbose, keep in mind that compared to the classic Android View Framework, you are merging logic and layout definitions into a single place and end up with less lines in total.

I do agree however that it is especially important in Compose to split Composables into smaller units.


Finally, an easy implementation of a login screen could look as follows:

@Composable
LoginComposable(loginViewModel: LoginViewModel = viewModel(), onNavigateToContent: () -> Unit) {

    var email by rememberSaveable { mutableStateOf("") }
    var password by rememberSaveable { mutableStateOf("") }

    LaunchedEffect(loginViewModel.successfulLogin) {
        if(loginViewModel.successfulLogin) {
            onNavigateToContent()
        }
    }

    Column(modifier = Modifier.fillMaxSize()) {

        TextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Enter email") },
            isError = loginViewModel.emailError,
            supportingText = if(loginViewModel.emailError) {
                Text(text = "Login failed")
            } else {}
        )

        TextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Enter password") },
            visualTransformation = PasswordVisualTransformation(),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
            isError = loginViewModel.passwordError,
            supportingText = if(loginViewModel.passwordError) {
                Text(text = "Login failed")
            } else {}
        )

        Button(onClick = { loginViewModel.login(email, password) }) {
            Text(text = "LOGIN")
        }

    }
}

And the ViewModel would look like this (userRepository is injected by a dependency injection framework):

class LoginViewModel(
    private val userRepository: UserRepository  // using dependency injection
) : ViewModel() {

    var successfulLogin by mutableStateOf(false)
    var emailError by mutableStateOf(false)
    var passwordError = mutableStateOf(false)

    fun login(email: String, password: String) {
        emailError = false
        passwordError = false

        viewModelScope.launch {
            try {
                userRepository.login(email, password)  // call networking suspend function
                successfulLogin = true
            } catch (e: Exception) {
                // TODO add more sophisticated error handling here
                emailError = true
                passwordError = true
            }
        }
    }
}

This should give you the idea of a simple implementation.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.