I have created two TextField Email, Password & Button Login. Now on click of that button I want to access text and show success/error based on validation.
The problem is they are in two different composable functions.
#Composable
fun EmailField() {
var email by remember { mutableStateOf("") }
TextField(
modifier = Modifier.fillMaxWidth(0.9f),
colors = TextFieldDefaults.textFieldColors(
textColor = Color.White,
focusedIndicatorColor = Color.White,
focusedLabelColor = Color.White
),
value = email,
onValueChange = { email = it },
label = { Text("Email") },
leadingIcon = {
Icon(
Icons.Filled.Email,
"contentDescription",
modifier = Modifier.clickable {})
}
)
}
Button:
#Composable
private fun LoginButton() {
Button(
onClick = {
// validate email and password here
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Yellow,
contentColor = Color.White
)
) {
Text(text = "Login")
}
}
If you want to see whole activity this is how it's structured at the moment.
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colors.primary),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(32.dp))
LoginLogo()
Spacer(modifier = Modifier.height(32.dp))
Text(
text = "Login",
modifier = Modifier.fillMaxWidth(0.9f),
style = MaterialTheme.typography.h5,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Please sign in to continue",
modifier = Modifier.fillMaxWidth(0.9f),
style = MaterialTheme.typography.subtitle1,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(32.dp))
EmailField()
Spacer(modifier = Modifier.height(16.dp))
PassWordField()
Spacer(modifier = Modifier.height(16.dp))
LoginButton()
}
}
}
}
#Composable
private fun LoginButton() {
Button(
onClick = {
// validate email and password here
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Yellow,
contentColor = Color.White
)
) {
Text(text = "Login")
}
}
#Composable
fun LoginLogo() {
Image(
painter = painterResource(R.drawable.ic_vector_app_logo),
contentDescription = "Login Logo",
modifier = Modifier
.width(120.dp)
.height(120.dp)
)
}
#Composable
fun EmailField() {
var email by remember { mutableStateOf("") }
TextField(
modifier = Modifier.fillMaxWidth(0.9f),
colors = TextFieldDefaults.textFieldColors(
textColor = Color.White,
focusedIndicatorColor = Color.White,
focusedLabelColor = Color.White
),
value = email,
onValueChange = { email = it },
label = { Text("Email") },
leadingIcon = {
Icon(
Icons.Filled.Email,
"contentDescription",
modifier = Modifier.clickable {})
}
)
}
#Composable
fun PassWordField() {
var password by rememberSaveable { mutableStateOf("") }
TextField(
modifier = Modifier.fillMaxWidth(0.9f),
colors = TextFieldDefaults.textFieldColors(
textColor = Color.White,
focusedIndicatorColor = Color.White,
focusedLabelColor = Color.White
),
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
leadingIcon = {
Icon(
Icons.Filled.Lock,
"contentDescription",
modifier = Modifier.clickable {})
}
)
}
}
What is the right way to handle values in this case?
You can use a ViewModel or something like:
class TextFieldState(){
var text: String by mutableStateOf("")
}
#Composable
fun login(){
var emailState = remember { TextFieldState() }
EmailField(emailState)
LoginButton( onValidate = { /** validate **/})
}
with:
#Composable
fun EmailField( emailState : TextFieldState = remember { TextFieldState() }) {
TextField(
value = emailState.text,
onValueChange = {
emailState.text = it
},
//your code
)
}
and:
#Composable
private fun LoginButton(onValidate: () -> Unit) {
Button(
onClick = onValidate,
//your code
)
}
Here is another solution with rememberSaveable
#Composable
fun MainBody() {
Column(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
var inputValue by rememberSaveable { mutableStateOf("") }
CommonTextField(value = inputValue, onInputChanged = { inputValue = it })
}
}
And here it is defined CommonTextField Function
#Composable
fun CommonTextField(value: String, onInputChanged: (String) -> Unit) {
TextField(value = value,
onValueChange = onInputChanged,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
label = { Text(text = "Enter here")})
}
I have achieved this with the following way :-
#Composable
fun login(){
var dataInput = "xyz#abc.com"
EmailField(dataInput, onChange = { dataInput = it })
LoginButton( onValidate = { /** validate **/})
}
#Composable
fun EmailField(textData: String, onChange: (String) -> Unit = {}) {
var textInput by remember { mutableStateOf(textData) }
TextField(
value = textInput,
onValueChange = {
textInput = it
onChange(it)
},
//your code
)
}
The beauty of this is that it will not recompose all the other views again on changing input for any TextField.
Related
I have created one function called TextInput that contained TextField
#Composable
fun TextInput(
inputType: InputType
) {
var value by remember { mutableStateOf("") }
TextField(
modifier = Modifier.clip(RoundedCornerShape(30.dp)),
value = value,
onValueChange = { value = it },
leadingIcon = { Icon(imageVector = inputType.icon, null) },
label = { Text(text = inputType.label) },
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
),
singleLine = true,
keyboardOptions = inputType.keyboardOptions,
visualTransformation = inputType.visualTransformation,
)
}
Now on click of the button SIGN UP\SIGN IN I want to access the value and show success/error based on validation.
Button(
onClick = {
//here
},
modifier = Modifier
.padding(top = 30.dp)
.width(200.dp),
shape = RoundedCornerShape(20.dp)
) {
Text(
text = "SIGN IN",
)
}
==================================================
Button(
onClick = {
//here
},
modifier = Modifier
.padding(top = 5.dp)
.width(200.dp),
shape = RoundedCornerShape(20.dp)
) {
Text(text = "SIGN UP")
}
If you want to see whole code to understand what i mean exactly.
#Composable
fun Sign_in_up_Screen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.login),
contentDescription = "",
modifier = Modifier
.background(Color.Companion.Red)
.fillMaxWidth()
)
TopTextButton()
}
}
#Composable
fun Login_Screen() {
val context = LocalContext.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp),
modifier = Modifier.fillMaxSize()
) {
TextInput(InputType.Email)
TextInput(InputType.Password)
Button(
onClick = {
//here
},
modifier = Modifier
.padding(top = 30.dp)
.width(200.dp),
shape = RoundedCornerShape(20.dp)
) {
Text(
text = "SIGN IN",
)
}
}
}
#Composable
fun Signup_Screen() {
val context = LocalContext.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp),
modifier = Modifier.fillMaxSize()
) {
TextInput(InputType.FullName)
TextInput(InputType.Email)
TextInput(InputType.Password)
TextInput(InputType.ConfirmPassword)
Button(
onClick = {
//here
},
modifier = Modifier
.padding(top = 5.dp)
.width(200.dp),
shape = RoundedCornerShape(20.dp)
) {
Text(text = "SIGN UP")
}
}
}
sealed class InputType(
val label: String,
val icon: ImageVector,
val keyboardOptions: KeyboardOptions,
val visualTransformation: VisualTransformation
) {
object FullName : InputType(
label = "Full Name",
icon = Icons.Default.Person,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
visualTransformation = VisualTransformation.None
)
object Email : InputType(
label = "E-mail",
icon = Icons.Default.Email,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
visualTransformation = VisualTransformation.None
)
object Password : InputType(
label = "Password",
icon = Icons.Default.Lock,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Password
),
visualTransformation = PasswordVisualTransformation()
)
object ConfirmPassword : InputType(
label = "Confirm Password",
icon = Icons.Default.Lock,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Password
),
visualTransformation = PasswordVisualTransformation()
)
}
#Composable
fun TextInput(
inputType: InputType
) {
var value by remember { mutableStateOf("") }
TextField(
modifier = Modifier.clip(RoundedCornerShape(30.dp)),
value = value,
onValueChange = { value = it },
leadingIcon = { Icon(imageVector = inputType.icon, null) },
label = { Text(text = inputType.label) },
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
),
singleLine = true,
keyboardOptions = inputType.keyboardOptions,
visualTransformation = inputType.visualTransformation,
)
}
#Composable
fun TopTextButton() {
var screen by remember { mutableStateOf(false) }
Row(
modifier = Modifier.padding(bottom = 40.dp),
) {
TextButton( onClick = {
if (screen)
screen = !screen
}) {
Text(text = "Sign in")
}
Spacer(Modifier.width(145.dp))
TextButton(onClick = {
if (!screen)
screen = !screen
}) {
Text(text = "Sign up")
}
}
if (!screen){
Login_Screen()
}
if (screen) {
Signup_Screen()
}
}
Is this code correct or should I put a function for each {Textfield}?
So each text field has its own state
I am beginner android studio programming😅
You can add onValueChange param into TextInput like this;
#Composable
fun TextInput(
inputType: InputType,
onValueChange: (String) -> Unit
) {
TextField(
//
onValueChange = {
onValueChange(it)
value = it
}
//
}
print your value;
TextInput(InputType.Email){ textFieldValue ->
println(textFieldValue)
}
I'm trying to put a drop down for US states that uses a TextField and a TextField in a Row and the second TextField (zip) does not show up. What am I doing wrong?
Here is how I'm declaring the row:
Row (Modifier.fillMaxWidth()) {
StateSelection(
onFormChanged = onFormChanged,
selectedLocation = selectedLocation,
label = "State"
)
Spacer(modifier = Modifier.width(8.dp))
ShippingField(
modifier = modifier,
onFormChanged = onFormChanged,
formType = FormType.SHIPPING_ZIP,
label = "Zip",
valueField = selectedLocation.zipCode
)
}
StatesDropDown:
#Composable
fun StateSelection(
onFormChanged: (FormType, String) -> Unit,
selectedLocation: Address,
label: String
) {
// State variables
val statesMap = AddressUtils.mapOfAmericanStatesToValue
var stateName: String by remember { mutableStateOf(selectedLocation.shippingState) }
var expanded by remember { mutableStateOf(false)}
val focusManager = LocalFocusManager.current
Box(contentAlignment = Alignment.CenterStart) {
Row(
Modifier
.padding(vertical = 8.dp)
.clickable {
expanded = !expanded
}
.padding(8.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) { // Anchor view
TextField(
modifier = Modifier
.fillMaxWidth(),
value = stateName,
onValueChange = {
onFormChanged(FormType.SHIPPING_COUNTRY, it)
},
label = { Text(text = label) },
textStyle = MaterialTheme.typography.subtitle1,
singleLine = true,
trailingIcon = {
IconButton(onClick = { expanded = true }) {
Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription = "")
}
},
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Text
),
colors = TextFieldDefaults.textFieldColors(
cursorColor = MaterialTheme.colors.secondary,
textColor = MaterialTheme.colors.onPrimary,
focusedIndicatorColor = MaterialTheme.colors.secondary,
backgroundColor = MaterialTheme.colors.secondaryVariant
)
) // state name label
DropdownMenu(expanded = expanded, onDismissRequest = {
expanded = false
}) {
statesMap.asIterable().iterator().forEach {
val (key, value) = it
DropdownMenuItem(onClick = {
expanded = false
stateName = key
onFormChanged(FormType.SHIPPING_STATE, key)
},
modifier = Modifier.fillMaxWidth()) {
Text(text = key)
}
}
}
}
}
}
ZipCode:
#Composable
fun ShippingField(
modifier: Modifier,
onFormChanged: (FormType, String) -> Unit,
formType: FormType,
label: String,
valueField: String
) {
val focusManager = LocalFocusManager.current
var state by rememberSaveable {
mutableStateOf(valueField)
}
TextField(modifier = modifier
.background(MaterialTheme.colors.primaryVariant)
.fillMaxWidth(),
value = state,
onValueChange = {
state = it
onFormChanged(formType, it)
},
label = { Text(text = label) },
textStyle = MaterialTheme.typography.subtitle1,
singleLine = true,
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Text
),
colors = TextFieldDefaults.textFieldColors(
cursorColor = MaterialTheme.colors.secondary,
textColor = MaterialTheme.colors.onPrimary,
focusedIndicatorColor = MaterialTheme.colors.secondary,
backgroundColor = MaterialTheme.colors.secondaryVariant
)
)
}
Row (Modifier.fillMaxWidth()) {
StateSelection(
modifier = Modifier.weight(1f)
...
)
Spacer(modifier = Modifier.width(8.dp))
ShippingField(
modifier = Modifier.weight(1f),
...
)
}
You should do it like above, and then pass the modifier to your composables below. Weight distributes and fills max width evenly on your composables
You have to remove the modifier = Modifier.fillMaxWidth() in the 1st TextField in the StatesDropDown composable.
I have a login page with two TextFields, one for the username and one for the password. When the submit button is clicked, I call my loginViewModel's login(username,passwd) method in order to asynchronously authenticate the user. Once the authentication result is received, I post the value to the ViewModel's mutableLiveData in order for the value to be observed in my Login composable through loginViewModel.wasLoginSuccessful.observeAsState(false).
The problem occurs with the if (isUserAuthenticated) onSuccessfulLogin() since when the authentication result is true, I successfully get "redirected" to the home page but everything is super laggy, and using the profiler I can see the Memory & CPU usage fluctuating a lot. If I remove the if statement and make it so that the onSuccessfullLogin() is called unconditionally, everything works just fine without any issues. I'm guessing that this is a side-effect through recomposition and/or a memory leak but I can't pinpoint it.
Login page:
#Composable
fun Login(navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
LoginContent() {
navController.navigate(Screens.DASHBOARD_MYEWAY.navRoute) {
popUpTo(0)
}
}
LoginFooter() {
navController.navigate(NavigationGraphs.ACTIVATION.route)
}
}
}
#Composable
private fun LoginContent(onSuccessfulLogin: () -> Unit) {
val loginViewModel: LoginViewModel = remember { getKoin().get() }
val isUserAuthenticated by loginViewModel.wasLoginSuccessful.observeAsState(false)
val username = remember { mutableStateOf("") }
val password = remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
TopBarPadding()
Spacer(modifier = Modifier.padding(24.fixedDp()))
Text(
text = LocalContext.current.getString(R.string.welcome_plural),
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.Black_100),
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.padding(8.fixedDp()))
Text(
text = LocalContext.current.getString(R.string.please_enter_password_to_proceed),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.Black_100)
)
Spacer(modifier = Modifier.padding(30.fixedDp()))
InputField(
label = LocalContext.current.getString(R.string.username),
onTextChangeCallback = { username.value = it })
Spacer(modifier = Modifier.padding(26.fixedDp()))
InputField(
label = LocalContext.current.getString(R.string.password),
isPassword = true,
canReveal = true,
onTextChangeCallback = { password.value = it })
Spacer(modifier = Modifier.padding(32.fixedDp()))
MyButton(text = LocalContext.current.getString(R.string.login),
buttonType = MyButtonType.PRIMARY,
onClick = {
scope.launch {
loginViewModel.login(username.value, password.value)
}
})
Spacer(modifier = Modifier.padding(24.fixedDp()))
Link(text = LocalContext.current.getString(R.string.forgot_my_password))
}
if (isUserAuthenticated) onSuccessfulLogin()
}
#Composable
private fun LoginFooter(onClickEwayActivation: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = LocalContext.current.getString(R.string.you_have_no_username),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.Black_100)
)
Row {
Text(
text = LocalContext.current.getString(R.string.do_string),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.Black_100)
)
Link(
text = LocalContext.current.getString(R.string.my_eway_activation),
onClick = { onClickEwayActivation() })
}
}
Spacer(modifier = Modifier.padding(40.fixedDp()))
}
}
LoginViewModel:
class LoginViewModel(
private val authenticationUseCase: AuthenticationControllerUseCase
) : ViewModel() {
private val _loginResult = MutableLiveData(false)
val wasLoginSuccessful: LiveData<Boolean> = _loginResult
fun login(username: String, password: String) {
viewModelScope.launch(Dispatchers.IO) {
when (val response = authenticationUseCase.validateUser(username, password)) {
is ResponseResult.Success -> {
response.data?.userLoginId!!.saveStringThroughKeystore(PreferenceKeys.ACCOUNT_ID.key)
_loginResult.postValue(true)
}
is ResponseResult.Error -> {
_loginResult.postValue(false)
Log.d(
"USER_VALIDATION_ERROR",
"Network Call Failed With Exception: " + response.exception.message
)
}
}
}
}
}
I have an application with sign in via email and password with firebase.
I'm using jetpack compose with MVVM and clean architecture.
When getting from firebase that login was done I'm getting true in the view model and then listening to this state change in the composable.
The problem is I'm always get in the when statement of the sign in state and it cause an infinite loop that always navigating to the next composable.
Copying here the specific line from the view:
when (val response = viewModel.signInState.value) {
is Response.Loading -> LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
is Response.Success -> if (response.data) {
navController?.navigate(Screen.HomeScreen.route)
}
is Response.Error -> LaunchedEffect(Unit) {
scaffoldState.snackbarHostState.showSnackbar("Error signing out. ${response.message}", "", SnackbarDuration.Short)
}
}
My View:
#Composable
fun LoginScreen(
navController: NavController?,
viewModel: LoginViewModel = hiltViewModel()
) {
val scaffoldState = rememberScaffoldState()
Scaffold(scaffoldState = scaffoldState) {
Column (
modifier = Modifier
.fillMaxHeight()
.background(
Color.White
),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
){
var email = viewModel.email
var pass = viewModel.password
var passwordVisibility = viewModel.passwordVisibility
TitleView(title = "SIGN IN", topImage = Icons.Filled.Person, modifier = Modifier)
Spacer(modifier = Modifier.padding(20.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
value = email.value,
onValueChange = viewModel::setEmail,
label = { Text( "Email")},
placeholder = { Text("Password") }
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
value = pass.value,
onValueChange = viewModel::setPassword,
label = { Text( "Password")},
placeholder = { Text("Password") },
visualTransformation = if (passwordVisibility.value) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
val image = if (passwordVisibility.value)
Icons.Filled.Visibility
else Icons.Filled.VisibilityOff
IconButton(onClick = {
viewModel.setPasswordVisibility(!passwordVisibility.value)
}) {
Icon(imageVector = image, "")
}
}
)
Spacer(modifier = Modifier.padding(5.dp))
Button(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.border(
width = 2.dp,
brush = SolidColor(Color.Yellow),
shape = RoundedCornerShape(size = 10.dp)
),
onClick = {
viewModel.signInWithEmailAndPassword()
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.White
)
) {
Text(
text = "SIGN IN",
textAlign = TextAlign.Center,
fontSize = 20.sp,
style = TextStyle(
fontWeight = FontWeight.Thin
)
)
}
Spacer(modifier = Modifier.padding(5.dp))
Button(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.border(
width = 2.dp,
brush = SolidColor(Color.Yellow),
shape = RoundedCornerShape(size = 10.dp)
),
onClick = {
viewModel.forgotPassword()
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.White
)
) {
Text(
text = "FORGOT PASSWORD",
textAlign = TextAlign.Center,
fontSize = 20.sp,
style = TextStyle(
fontWeight = FontWeight.Thin
)
)
}
}
}
}
when (val response = viewModel.signInState.value) {
is Response.Loading -> LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
is Response.Success -> if (response.data) {
navController?.navigate(Screen.HomeScreen.route)
}
is Response.Error -> LaunchedEffect(Unit) {
scaffoldState.snackbarHostState.showSnackbar("Error signing out. ${response.message}", "", SnackbarDuration.Short)
}
}
when (val response = viewModel.forgotState.value) {
is Response.Loading -> LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
is Response.Success -> if (response.data) {
LaunchedEffect(Unit){
scaffoldState.snackbarHostState.showSnackbar("Please click the link in the verification email sent to you", "", SnackbarDuration.Short)
}
}
is Response.Error -> LaunchedEffect(Unit){
scaffoldState.snackbarHostState.showSnackbar("Error signing out. ${response.message}", "", SnackbarDuration.Short)
}
}
}
My View model:
#HiltViewModel
class LoginViewModel #Inject constructor(
private val useCases: UseCases
) : ViewModel() {
private val _signInState = mutableStateOf<Response<Boolean>>(Response.Success(false))
val signInState: State<Response<Boolean>> = _signInState
private val _forgotState = mutableStateOf<Response<Boolean>>(Response.Success(false))
val forgotState: State<Response<Boolean>> = _forgotState
private val _email = mutableStateOf("")
val email = _email
fun setEmail(email: String) {
_email.value = email
}
private val _password = mutableStateOf("")
val password = _password
fun setPassword(password: String) {
_password.value = password
}
private val _passwordVisibility = mutableStateOf(false)
val passwordVisibility = _passwordVisibility
fun setPasswordVisibility(passwordVisibility: Boolean) {
_passwordVisibility.value = passwordVisibility
}
fun signInWithEmailAndPassword() {
viewModelScope.launch {
useCases.signInWithEmailAndPassword(_email.value, _password.value).collect { response ->
_signInState.value = response
}
}
}
fun forgotPassword() {
viewModelScope.launch {
useCases.forgotPassword(_email.value).collect { response ->
_forgotState.value = response
}
}
}
}
My Sign in function:
override suspend fun signInWithEmailAndPassword(
email: String,
password: String
) = flow {
try {
emit(Response.Loading)
val result = auth.signInWithEmailAndPassword(email, password).await()
if (result.user != null)
emit(Response.Success(true))
else
emit(Response.Success(false))
} catch (e: Exception) {
emit(Response.Error(e.message ?: ERROR_MESSAGE))
}
}
Navigation is also a side effect, after each frame of the animation you will again navigate to your destination. Change your block to:
when (val response = viewModel.signInState.value) {
is Response.Loading -> LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
is Response.Success -> if (response.data) LaunchedEffect(response.data){ navController?.navigate(Screen.HomeScreen.route) }
is Response.Error -> LaunchedEffect(Unit) {
scaffoldState.snackbarHostState.showSnackbar("Error signing out. ${response.message}", "", SnackbarDuration.Short)
}
}
I was making a login for my app in the new android jetpack's compose.
I want to make a OTP layout like in the given photo.
check full example here
const val PIN_VIEW_TYPE_UNDERLINE = 0
const val PIN_VIEW_TYPE_BORDER = 1
#Composable
fun PinView(
pinText: String,
onPinTextChange: (String) -> Unit,
digitColor: Color = MaterialTheme.colors.onBackground,
digitSize: TextUnit = 16.sp,
containerSize: Dp = digitSize.value.dp * 2,
digitCount: Int = 4,
type: Int = PIN_VIEW_TYPE_UNDERLINE,
) {
BasicTextField(value = pinText,
onValueChange = onPinTextChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
decorationBox = {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
repeat(digitCount) { index ->
DigitView(index, pinText, digitColor, digitSize, containerSize, type = type)
Spacer(modifier = Modifier.width(5.dp))
}
}
})
}
#Composable
private fun DigitView(
index: Int,
pinText: String,
digitColor: Color,
digitSize: TextUnit,
containerSize: Dp,
type: Int = PIN_VIEW_TYPE_UNDERLINE,
) {
val modifier = if (type == PIN_VIEW_TYPE_BORDER) {
Modifier
.width(containerSize)
.border(
width = 1.dp,
color = digitColor,
shape = MaterialTheme.shapes.medium
)
.padding(bottom = 3.dp)
} else Modifier.width(containerSize)
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center) {
Text(
text = if (index >= pinText.length) "" else pinText[index].toString(),
color = digitColor,
modifier = modifier,
style = MaterialTheme.typography.body1,
fontSize = digitSize,
textAlign = TextAlign.Center)
if (type == PIN_VIEW_TYPE_UNDERLINE) {
Spacer(modifier = Modifier.height(2.dp))
Box(
modifier = Modifier
.background(digitColor)
.height(1.dp)
.width(containerSize)
)
}
}
}
You can use a very simple layout for each char in the otp.
Something like
#Composable
fun OtpChar(){
var text by remember { mutableStateOf("1") }
val maxChar = 1
Column(Modifier.background(DarkGray),
horizontalAlignment = Alignment.CenterHorizontally){
TextField(
value =text,
onValueChange = {if (it.length <= maxChar) text = it},
modifier = Modifier.width(50.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(
fontSize = 20.sp,
textAlign= TextAlign.Center),
colors= TextFieldDefaults.textFieldColors(
textColor = White,
backgroundColor = Transparent,
unfocusedIndicatorColor = Transparent,
focusedIndicatorColor = Transparent)
)
Divider(Modifier
.width(28.dp)
.padding(bottom = 2.dp)
.offset(y=-10.dp),
color = White,
thickness = 1.dp)
}
}
You can add some features like:
manage the focus in Next direction with the TAB key
manage the focus in Previous direction with the BACK SPACE key
how to move to the next textfield when a digit is entered
Something like:
fun OtpChar(
modifier: Modifier = Modifier
){
val pattern = remember { Regex("^[^\\t]*\$") } //to not accept the tab key as value
var (text,setText) = remember { mutableStateOf("") }
val maxChar = 1
val focusManager = LocalFocusManager.current
LaunchedEffect(
key1 = text,
) {
if (text.isNotEmpty()) {
focusManager.moveFocus(
focusDirection = FocusDirection.Next,
)
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
){
TextField(
value =text,
onValueChange = {
if (it.length <= maxChar &&
((it.isEmpty() || it.matches(pattern))))
setText(it)
},
modifier = modifier
.width(50.dp)
.onKeyEvent {
if (it.key == Key.Tab) {
focusManager.moveFocus(FocusDirection.Next)
true
}
if (text.isEmpty() && it.key == Key.Backspace) {
focusManager.moveFocus(FocusDirection.Previous)
}
false
},
textStyle = LocalTextStyle.current.copy(
fontSize = 20.sp,
textAlign= TextAlign.Center),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
colors= TextFieldDefaults.textFieldColors(
backgroundColor = Transparent,
unfocusedIndicatorColor = Transparent,
focusedIndicatorColor = Transparent),
)
Divider(
Modifier
.width(28.dp)
.padding(bottom = 2.dp)
.offset(y = -10.dp),
color = Teal200,
thickness = 1.dp)
}
}
Then just use something like a Row to display 4 OtpChars
val (item1, item2, item3, item4) = FocusRequester.createRefs()
Row(horizontalArrangement = Arrangement.SpaceBetween){
OtpChar(
modifier = Modifier
.focusRequester(item1)
.focusProperties {
next = item2
previous = item1
}
)
OtpChar(
modifier = Modifier
.focusRequester(item2)
.focusProperties {
next = item3
previous = item1
}
)
OtpChar(
modifier = Modifier
.focusRequester(item3)
.focusProperties {
next = item4
previous = item2
}
)
OtpChar(
modifier = Modifier
.focusRequester(item4)
.focusProperties {
previous = item3
next = item4
}
)
//....
}
If you faced keyboard issues try the code below:
#Composable
fun OtpCell(
modifier: Modifier,
value: String,
isCursorVisible: Boolean = false
) {
val scope = rememberCoroutineScope()
val (cursorSymbol, setCursorSymbol) = remember { mutableStateOf("") }
LaunchedEffect(key1 = cursorSymbol, isCursorVisible) {
if (isCursorVisible) {
scope.launch {
delay(350)
setCursorSymbol(if (cursorSymbol.isEmpty()) "|" else "")
}
}
}
Box(
modifier = modifier
) {
Text(
text = if (isCursorVisible) cursorSymbol else value,
style = MaterialTheme.typography.body1,
modifier = Modifier.align(Alignment.Center)
)
}
}
#ExperimentalComposeUiApi
#Composable
fun PinInput(
modifier: Modifier = Modifier,
length: Int = 5,
value: String = "",
onValueChanged: (String) -> Unit
) {
val focusRequester = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current
TextField(
value = value,
onValueChange = {
if (it.length <= length) {
if (it.all { c -> c in '0'..'9' }) {
onValueChanged(it)
}
if (it.length >= length) {
keyboard?.hide()
}
}
},
// Hide the text field
modifier = Modifier
.size(0.dp)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
repeat(length) {
OtpCell(
modifier = modifier
.size(width = 45.dp, height = 60.dp)
.clip(MaterialTheme.shapes.large)
.background(MaterialTheme.colors.surface)
.clickable {
focusRequester.requestFocus()
keyboard?.show()
},
value = value.getOrNull(it)?.toString() ?: "",
isCursorVisible = value.length == it
)
if (it != length - 1) Spacer(modifier = Modifier.size(8.dp))
}
}
}
Result:
First make the common TextField for OTP Screen
#Composable
fun CommonOtpTextField(otp: MutableState<String>) {
val max = 1
OutlinedTextField(
value = otp.value,
singleLine = true,
onValueChange = { if (it.length <= max) otp.value = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.width(60.dp)
.height(60.dp),
maxLines = 1,
textStyle = LocalTextStyle.current.copy(
textAlign = TextAlign.Center
)
)
}
And Now use above CommonTextField to make four otp Field
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 15.dp, start = 15.dp, end = 15.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
CommonOtpTextField(otp = otpOne)
CommonOtpTextField(otp = otpTwo)
CommonOtpTextField(otp = otpThree)
CommonOtpTextField(otp = otpFour)
}
Simple solution that uses one TextField and different code length
#Composable
fun RegistrationCodeInput(codeLength: Int) {
val code = remember { mutableStateOf("") }
val focusRequester = FocusRequester()
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
BasicTextField(
value = code.value,
onValueChange = { if (it.length <= codeLength) code.value = it },
Modifier.focusRequester(focusRequester = focusRequester),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
decorationBox = {
CodeInputDecoration(code.value, codeLength)
})
}
}
#Composable
private fun CodeInputDecoration(code: String, length: Int) {
Box(
modifier = Modifier
.padding(16.dp)
.border(
border = BorderStroke(2.dp, color = borderColor),
shape = Shapes.small
)
) {
Row(
) {
for (i in 0 until length) {
val text = if (i < code.length) code[i].toString() else ""
CodeEntry(text)
}
}
}
}
#Composable
private fun CodeEntry(text: String) {
Box(
modifier = Modifier
.width(42.dp)
.height(42.dp),
contentAlignment = Alignment.Center
) {
Text(text = text)
}
}
#Preview
#Composable
fun PreviewInput() {
RegistrationCodeInput(4)
}